Create and modify PDF documents in any JavaScript environment.
Designed to work in any modern JavaScript runtime. Tested in Node, Browser, Deno, and React Native environments.
Learn more at pdf-lib.js.org
Table of Contents
Features
- Create new PDFs
- Modify existing PDFs
- Create forms
- Fill forms
- Flatten forms
- Add Pages
- Insert Pages
- Remove Pages
- Copy pages between PDFs
- Draw Text
- Draw Images
- Draw PDF Pages
- Draw Vector Graphics
- Draw SVG Paths
- Measure width and height of text
- Embed Fonts (supports UTF-8 and UTF-16 character sets)
- Set document metadata
- Read document metadata
- Set viewer preferences
- Read viewer preferences
- Add attachments
Motivation
pdf-lib
was created to address the JavaScript ecosystem's lack of robust support for PDF manipulation (especially for PDF modification).
Two of pdf-lib
's distinguishing features are:
- Supporting modification (editing) of existing documents.
- Working in all JavaScript environments - not just in Node or the Browser.
There are other good open source JavaScript PDF libraries available. However, most of them can only create documents, they cannot modify existing ones. And many of them only work in particular environments.
Usage Examples
Create Document
This example produces this PDF.
Try the JSFiddle demo
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
const page = pdfDoc.addPage()
const { width, height } = page.getSize()
const fontSize = 30
page.drawText('Creating PDFs in JavaScript is awesome!', {
x: 50,
y: height - 4 * fontSize,
size: fontSize,
font: timesRomanFont,
color: rgb(0, 0.53, 0.71),
})
const pdfBytes = await pdfDoc.save()
Modify Document
This example produces this PDF (when this PDF is used for the existingPdfBytes
variable).
Try the JSFiddle demo
import { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';
const existingPdfBytes = ...
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
const pages = pdfDoc.getPages()
const firstPage = pages[0]
const { width, height } = firstPage.getSize()
firstPage.drawText('This text was added with JavaScript!', {
x: 5,
y: height / 2 + 300,
size: 50,
font: helveticaFont,
color: rgb(0.95, 0.1, 0.1),
rotate: degrees(-45),
})
const pdfBytes = await pdfDoc.save()
Create Form
This example produces this PDF.
Try the JSFiddle demo
See also Creating and Filling Forms
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage([550, 750])
const form = pdfDoc.getForm()
page.drawText('Enter your favorite superhero:', { x: 50, y: 700, size: 20 })
const superheroField = form.createTextField('favorite.superhero')
superheroField.setText('One Punch Man')
superheroField.addToPage(page, { x: 55, y: 640 })
page.drawText('Select your favorite rocket:', { x: 50, y: 600, size: 20 })
page.drawText('Falcon Heavy', { x: 120, y: 560, size: 18 })
page.drawText('Saturn IV', { x: 120, y: 500, size: 18 })
page.drawText('Delta IV Heavy', { x: 340, y: 560, size: 18 })
page.drawText('Space Launch System', { x: 340, y: 500, size: 18 })
const rocketField = form.createRadioGroup('favorite.rocket')
rocketField.addOptionToPage('Falcon Heavy', page, { x: 55, y: 540 })
rocketField.addOptionToPage('Saturn IV', page, { x: 55, y: 480 })
rocketField.addOptionToPage('Delta IV Heavy', page, { x: 275, y: 540 })
rocketField.addOptionToPage('Space Launch System', page, { x: 275, y: 480 })
rocketField.select('Saturn IV')
page.drawText('Select your favorite gundams:', { x: 50, y: 440, size: 20 })
page.drawText('Exia', { x: 120, y: 400, size: 18 })
page.drawText('Kyrios', { x: 120, y: 340, size: 18 })
page.drawText('Virtue', { x: 340, y: 400, size: 18 })
page.drawText('Dynames', { x: 340, y: 340, size: 18 })
const exiaField = form.createCheckBox('gundam.exia')
const kyriosField = form.createCheckBox('gundam.kyrios')
const virtueField = form.createCheckBox('gundam.virtue')
const dynamesField = form.createCheckBox('gundam.dynames')
exiaField.addToPage(page, { x: 55, y: 380 })
kyriosField.addToPage(page, { x: 55, y: 320 })
virtueField.addToPage(page, { x: 275, y: 380 })
dynamesField.addToPage(page, { x: 275, y: 320 })
exiaField.check()
dynamesField.check()
page.drawText('Select your favorite planet*:', { x: 50, y: 280, size: 20 })
const planetsField = form.createDropdown('favorite.planet')
planetsField.addOptions(['Venus', 'Earth', 'Mars', 'Pluto'])
planetsField.select('Pluto')
planetsField.addToPage(page, { x: 55, y: 220 })
page.drawText('Select your favorite person:', { x: 50, y: 180, size: 18 })
const personField = form.createOptionList('favorite.person')
personField.addOptions([
'Julius Caesar',
'Ada Lovelace',
'Cleopatra',
'Aaron Burr',
'Mark Antony',
])
personField.select('Ada Lovelace')
personField.addToPage(page, { x: 55, y: 70 })
page.drawText(`* Pluto should be a planet too!`, { x: 15, y: 15, size: 15 })
const pdfBytes = await pdfDoc.save()
Fill Form
This example produces this PDF (when this PDF is used for the formPdfBytes
variable, this image is used for the marioImageBytes
variable, and this image is used for the emblemImageBytes
variable).
Try the JSFiddle demo
See also Creating and Filling Forms
import { PDFDocument } from 'pdf-lib'
const formPdfBytes = ...
const marioImageBytes = ...
const emblemImageBytes = ...
const pdfDoc = await PDFDocument.load(formPdfBytes)
const marioImage = await pdfDoc.embedPng(marioImageBytes)
const emblemImage = await pdfDoc.embedPng(emblemImageBytes)
const form = pdfDoc.getForm()
const nameField = form.getTextField('CharacterName 2')
const ageField = form.getTextField('Age')
const heightField = form.getTextField('Height')
const weightField = form.getTextField('Weight')
const eyesField = form.getTextField('Eyes')
const skinField = form.getTextField('Skin')
const hairField = form.getTextField('Hair')
const alliesField = form.getTextField('Allies')
const factionField = form.getTextField('FactionName')
const backstoryField = form.getTextField('Backstory')
const traitsField = form.getTextField('Feat+Traits')
const treasureField = form.getTextField('Treasure')
const characterImageField = form.getButton('CHARACTER IMAGE')
const factionImageField = form.getTextField('Faction Symbol Image')
nameField.setText('Mario')
ageField.setText('24 years')
heightField.setText(`5' 1"`)
weightField.setText('196 lbs')
eyesField.setText('blue')
skinField.setText('white')
hairField.setText('brown')
characterImageField.setImage(marioImage)
alliesField.setText(
[
`Allies:`,
` • Princess Daisy`,
` • Princess Peach`,
` • Rosalina`,
` • Geno`,
` • Luigi`,
` • Donkey Kong`,
` • Yoshi`,
` • Diddy Kong`,
``,
`Organizations:`,
` • Italian Plumbers Association`,
].join('\n'),
)
factionField.setText(`Mario's Emblem`)
factionImageField.setImage(emblemImage)
backstoryField.setText(
`Mario is a fictional character in the Mario video game franchise, owned by Nintendo and created by Japanese video game designer Shigeru Miyamoto. Serving as the company's mascot and the eponymous protagonist of the series, Mario has appeared in over 200 video games since his creation. Depicted as a short, pudgy, Italian plumber who resides in the Mushroom Kingdom, his adventures generally center upon rescuing Princess Peach from the Koopa villain Bowser. His younger brother and sidekick is Luigi.`,
)
traitsField.setText(
[
`Mario can use three basic three power-ups:`,
` • the Super Mushroom, which causes Mario to grow larger`,
` • the Fire Flower, which allows Mario to throw fireballs`,
` • the Starman, which gives Mario temporary invincibility`,
].join('\n'),
)
treasureField.setText(['• Gold coins', '• Treasure chests'].join('\n'))
const pdfBytes = await pdfDoc.save()
Flatten Form
This example produces this PDF (when this PDF is used for the formPdfBytes
variable).
Try the JSFiddle demo
import { PDFDocument } from 'pdf-lib'
const formPdfBytes = ...
const pdfDoc = await PDFDocument.load(formPdfBytes)
const form = pdfDoc.getForm()
form.getTextField('Text1').setText('Some Text');
form.getRadioGroup('Group2').select('Choice1');
form.getRadioGroup('Group3').select('Choice3');
form.getRadioGroup('Group4').select('Choice1');
form.getCheckBox('Check Box3').check();
form.getCheckBox('Check Box4').uncheck();
form.getDropdown('Dropdown7').select('Infinity');
form.getOptionList('List Box6').select('Honda');
form.flatten();
const pdfBytes = await pdfDoc.save()
Copy Pages
This example produces this PDF (when this PDF is used for the firstDonorPdfBytes
variable and this PDF is used for the secondDonorPdfBytes
variable).
Try the JSFiddle demo
import { PDFDocument } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const firstDonorPdfBytes = ...
const secondDonorPdfBytes = ...
const firstDonorPdfDoc = await PDFDocument.load(firstDonorPdfBytes)
const secondDonorPdfDoc = await PDFDocument.load(secondDonorPdfBytes)
const [firstDonorPage] = await pdfDoc.copyPages(firstDonorPdfDoc, [0])
const [secondDonorPage] = await pdfDoc.copyPages(secondDonorPdfDoc, [742])
pdfDoc.addPage(firstDonorPage)
pdfDoc.insertPage(0, secondDonorPage)
const pdfBytes = await pdfDoc.save()
Embed PNG and JPEG Images
This example produces this PDF (when this image is used for the jpgImageBytes
variable and this image is used for the pngImageBytes
variable).
Try the JSFiddle demo
import { PDFDocument } from 'pdf-lib'
const jpgImageBytes = ...
const pngImageBytes = ...
const pdfDoc = await PDFDocument.create()
const jpgImage = await pdfDoc.embedJpg(jpgImageBytes)
const pngImage = await pdfDoc.embedPng(pngImageBytes)
const jpgDims = jpgImage.scale(0.25)
const pngDims = pngImage.scale(0.5)
const page = pdfDoc.addPage()
page.drawImage(jpgImage, {
x: page.getWidth() / 2 - jpgDims.width / 2,
y: page.getHeight() / 2 - jpgDims.height / 2,
width: jpgDims.width,
height: jpgDims.height,
})
page.drawImage(pngImage, {
x: page.getWidth() / 2 - pngDims.width / 2 + 75,
y: page.getHeight() / 2 - pngDims.height,
width: pngDims.width,
height: pngDims.height,
})
const pdfBytes = await pdfDoc.save()
Embed PDF Pages
This example produces this PDF (when this PDF is used for the americanFlagPdfBytes
variable and this PDF is used for the usConstitutionPdfBytes
variable).
Try the JSFiddle demo
import { PDFDocument } from 'pdf-lib'
const americanFlagPdfBytes = ...
const usConstitutionPdfBytes = ...
const pdfDoc = await PDFDocument.create()
const [americanFlag] = await pdfDoc.embedPdf(americanFlagPdfBytes)
const usConstitutionPdf = await PDFDocument.load(usConstitutionPdfBytes)
const preamble = await pdfDoc.embedPage(usConstitutionPdf.getPages()[1], {
left: 55,
bottom: 485,
right: 300,
top: 575,
})
const americanFlagDims = americanFlag.scale(0.3)
const preambleDims = preamble.scale(2.25)
const page = pdfDoc.addPage()
page.drawPage(americanFlag, {
...americanFlagDims,
x: page.getWidth() / 2 - americanFlagDims.width / 2,
y: page.getHeight() - americanFlagDims.height - 150,
})
page.drawPage(preamble, {
...preambleDims,
x: page.getWidth() / 2 - preambleDims.width / 2,
y: page.getHeight() / 2 - preambleDims.height / 2 - 50,
})
const pdfBytes = await pdfDoc.save()
Embed Font and Measure Text
pdf-lib
relies on a sister module to support embedding custom fonts: @pdf-lib/fontkit
. You must add the @pdf-lib/fontkit
module to your project and register it using pdfDoc.registerFontkit(...)
before embedding custom fonts.
See below for detailed installation instructions on installing @pdf-lib/fontkit
as a UMD or NPM module.
This example produces this PDF (when this font is used for the fontBytes
variable).
Try the JSFiddle demo
import { PDFDocument, rgb } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
const fontBytes = ...
const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)
const page = pdfDoc.addPage()
const text = 'This is text in an embedded font!'
const textSize = 35
const textWidth = customFont.widthOfTextAtSize(text, textSize)
const textHeight = customFont.heightAtSize(textSize)
page.drawText(text, {
x: 40,
y: 450,
size: textSize,
font: customFont,
color: rgb(0, 0.53, 0.71),
})
page.drawRectangle({
x: 40,
y: 450,
width: textWidth,
height: textHeight,
borderColor: rgb(1, 0, 0),
borderWidth: 1.5,
})
const pdfBytes = await pdfDoc.save()
Add Attachments
This example produces this PDF (when this image is used for the jpgAttachmentBytes
variable and this PDF is used for the pdfAttachmentBytes
variable).
Try the JSFiddle demo
import { PDFDocument } from 'pdf-lib'
const jpgAttachmentBytes = ...
const pdfAttachmentBytes = ...
const pdfDoc = await PDFDocument.create()
await pdfDoc.attach(jpgAttachmentBytes, 'cat_riding_unicorn.jpg', {
mimeType: 'image/jpeg',
description: 'Cool cat riding a unicorn! 🦄🐈🕶️',
creationDate: new Date('2019/12/01'),
modificationDate: new Date('2020/04/19'),
})
await pdfDoc.attach(pdfAttachmentBytes, 'us_constitution.pdf', {
mimeType: 'application/pdf',
description: 'Constitution of the United States 🇺🇸🦅',
creationDate: new Date('1787/09/17'),
modificationDate: new Date('1992/05/07'),
})
const page = pdfDoc.addPage();
page.drawText('This PDF has two attachments', { x: 135, y: 415 })
const pdfBytes = await pdfDoc.save()
Set Document Metadata
This example produces this PDF.
Try the JSFiddle demo
import { PDFDocument, StandardFonts } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
const page = pdfDoc.addPage([500, 600])
page.setFont(timesRomanFont)
page.drawText('The Life of an Egg', { x: 60, y: 500, size: 50 })
page.drawText('An Epic Tale of Woe', { x: 125, y: 460, size: 25 })
pdfDoc.setTitle('🥚 The Life of an Egg 🍳')
pdfDoc.setAuthor('Humpty Dumpty')
pdfDoc.setSubject('📘 An Epic Tale of Woe 📖')
pdfDoc.setKeywords(['eggs', 'wall', 'fall', 'king', 'horses', 'men'])
pdfDoc.setProducer('PDF App 9000 🤖')
pdfDoc.setCreator('pdf-lib (https://github.com/Hopding/pdf-lib)')
pdfDoc.setCreationDate(new Date('2018-06-24T01:58:37.228Z'))
pdfDoc.setModificationDate(new Date('2019-12-21T07:00:11.000Z'))
const pdfBytes = await pdfDoc.save()
Read Document Metadata
Try the JSFiddle demo
import { PDFDocument } from 'pdf-lib'
const existingPdfBytes = ...
const pdfDoc = await PDFDocument.load(existingPdfBytes, {
updateMetadata: false
})
console.log('Title:', pdfDoc.getTitle())
console.log('Author:', pdfDoc.getAuthor())
console.log('Subject:', pdfDoc.getSubject())
console.log('Creator:', pdfDoc.getCreator())
console.log('Keywords:', pdfDoc.getKeywords())
console.log('Producer:', pdfDoc.getProducer())
console.log('Creation Date:', pdfDoc.getCreationDate())
console.log('Modification Date:', pdfDoc.getModificationDate())
This script outputs the following (when this PDF is used for the existingPdfBytes
variable):
Title: Microsoft Word - Basic Curriculum Vitae example.doc
Author: Administrator
Subject: undefined
Creator: PScript5.dll Version 5.2
Keywords: undefined
Producer: Acrobat Distiller 8.1.0 (Windows)
Creation Date: 2010-07-29T14:26:00.000Z
Modification Date: 2010-07-29T14:26:00.000Z
Set Viewer Preferences
import {
PDFDocument,
StandardFonts,
NonFullScreenPageMode,
ReadingDirection,
PrintScaling,
Duplex,
PDFName,
} from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
const page = pdfDoc.addPage([500, 600])
page.setFont(timesRomanFont)
page.drawText('The Life of an Egg', { x: 60, y: 500, size: 50 })
page.drawText('An Epic Tale of Woe', { x: 125, y: 460, size: 25 })
const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
viewerPrefs.setHideToolbar(true)
viewerPrefs.setHideMenubar(true)
viewerPrefs.setHideWindowUI(true)
viewerPrefs.setFitWindow(true)
viewerPrefs.setCenterWindow(true)
viewerPrefs.setDisplayDocTitle(true)
pdfDoc.catalog.set(PDFName.of('PageMode'), PDFName.of('FullScreen'))
viewerPrefs.setNonFullScreenPageMode(NonFullScreenPageMode.UseOutlines)
viewerPrefs.setReadingDirection(ReadingDirection.L2R)
viewerPrefs.setPrintScaling(PrintScaling.None)
viewerPrefs.setDuplex(Duplex.DuplexFlipLongEdge)
viewerPrefs.setPickTrayByPDFSize(true)
viewerPrefs.setPrintPageRange({ start: 0, end: 0 })
viewerPrefs.setPrintPageRange([
{ start: 0, end: 0 },
{ start: 2, end: 2 },
{ start: 4, end: 6 },
])
viewerPrefs.setNumCopies(2)
const pdfBytes = await pdfDoc.save()
Read Viewer Preferences
import { PDFDocument } from 'pdf-lib'
const existingPdfBytes = ...
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
console.log('HideToolbar:', viewerPrefs.getHideToolbar())
console.log('HideMenubar:', viewerPrefs.getHideMenubar())
console.log('HideWindowUI:', viewerPrefs.getHideWindowUI())
console.log('FitWindow:', viewerPrefs.getFitWindow())
console.log('CenterWindow:', viewerPrefs.getCenterWindow())
console.log('DisplayDocTitle:', viewerPrefs.getDisplayDocTitle())
console.log('NonFullScreenPageMode:', viewerPrefs.getNonFullScreenPageMode())
console.log('ReadingDirection:', viewerPrefs.getReadingDirection())
console.log('PrintScaling:', viewerPrefs.getPrintScaling())
console.log('Duplex:', viewerPrefs.getDuplex())
console.log('PickTrayByPDFSize:', viewerPrefs.getPickTrayByPDFSize())
console.log('PrintPageRange:', viewerPrefs.getPrintPageRange())
console.log('NumCopies:', viewerPrefs.getNumCopies())
This script outputs the following (when this PDF is used for the existingPdfBytes
variable):
HideToolbar: true
HideMenubar: true
HideWindowUI: false
FitWindow: true
CenterWindow: true
DisplayDocTitle: true
NonFullScreenPageMode: UseNone
ReadingDirection: R2L
PrintScaling: None
Duplex: DuplexFlipLongEdge
PickTrayByPDFSize: true
PrintPageRange: [ { start: 1, end: 1 }, { start: 3, end: 4 } ]
NumCopies: 2
Draw SVG Paths
This example produces this PDF.
Try the JSFiddle demo
import { PDFDocument, rgb } from 'pdf-lib'
const svgPath =
'M 0,20 L 100,160 Q 130,200 150,120 C 190,-40 200,200 300,150 L 400,90'
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage()
page.moveTo(100, page.getHeight() - 5)
page.moveDown(25)
page.drawSvgPath(svgPath)
page.moveDown(200)
page.drawSvgPath(svgPath, { borderColor: rgb(0, 1, 0), borderWidth: 5 })
page.moveDown(200)
page.drawSvgPath(svgPath, { color: rgb(1, 0, 0) })
page.moveDown(200)
page.drawSvgPath(svgPath, { scale: 0.5 })
const pdfBytes = await pdfDoc.save()
Deno Usage
pdf-lib
fully supports the exciting new Deno runtime! All of the usage examples work in Deno. The only thing you need to do is change the imports for pdf-lib
and @pdf-lib/fontkit
to use the Skypack CDN, because Deno requires all modules to be referenced via URLs.
See also How to Create and Modify PDF Files in Deno With pdf-lib
Creating a Document with Deno
Below is the create document example modified for Deno:
import {
PDFDocument,
StandardFonts,
rgb,
} from 'https://cdn.skypack.dev/pdf-lib@^1.11.1?dts';
const pdfDoc = await PDFDocument.create();
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);
const page = pdfDoc.addPage();
const { width, height } = page.getSize();
const fontSize = 30;
page.drawText('Creating PDFs in JavaScript is awesome!', {
x: 50,
y: height - 4 * fontSize,
size: fontSize,
font: timesRomanFont,
color: rgb(0, 0.53, 0.71),
});
const pdfBytes = await pdfDoc.save();
await Deno.writeFile('out.pdf', pdfBytes);
If you save this script as create-document.ts
, you can execute it using Deno with the following command:
deno run --allow-write create-document.ts
The resulting out.pdf
file will look like this PDF.
Embedding a Font with Deno
Here's a slightly more complicated example demonstrating how to embed a font and measure text in Deno:
import {
degrees,
PDFDocument,
rgb,
StandardFonts,
} from 'https://cdn.skypack.dev/pdf-lib@^1.11.1?dts';
import fontkit from 'https://cdn.skypack.dev/@pdf-lib/fontkit@^1.0.0?dts';
const url = 'https://pdf-lib.js.org/assets/ubuntu/Ubuntu-R.ttf';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());
const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit);
const customFont = await pdfDoc.embedFont(fontBytes);
const page = pdfDoc.addPage();
const text = 'This is text in an embedded font!';
const textSize = 35;
const textWidth = customFont.widthOfTextAtSize(text, textSize);
const textHeight = customFont.heightAtSize(textSize);
page.drawText(text, {
x: 40,
y: 450,
size: textSize,
font: customFont,
color: rgb(0, 0.53, 0.71),
});
page.drawRectangle({
x: 40,
y: 450,
width: textWidth,
height: textHeight,
borderColor: rgb(1, 0, 0),
borderWidth: 1.5,
});
const pdfBytes = await pdfDoc.save();
await Deno.writeFile('out.pdf', pdfBytes);
If you save this script as custom-font.ts
, you can execute it with the following command:
deno run --allow-write --allow-net custom-font.ts
The resulting out.pdf
file will look like this PDF.
Complete Examples
The usage examples provide code that is brief and to the point, demonstrating the different features of pdf-lib
. You can find complete working examples in the apps/
directory. These apps are used to do manual testing of pdf-lib
before every release (in addition to the automated tests).
There are currently four apps:
node
- contains tests for pdf-lib
in Node environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib
from the filesystem. They also allow you to quickly open your PDFs in different viewers (Acrobat, Preview, Foxit, Chrome, Firefox, etc...) to ensure compatibility.web
- contains tests for pdf-lib
in browser environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib
in a browser environment.rn
- contains tests for pdf-lib
in React Native environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib
in a React Native environment.deno
- contains tests for pdf-lib
in Deno environments. These tests are a handy reference when trying to save/load PDFs, fonts, or images with pdf-lib
from the filesystem.
Installation
NPM Module
To install the latest stable version:
npm install --save pdf-lib
yarn add pdf-lib
This assumes you're using npm or yarn as your package manager.
UMD Module
You can also download pdf-lib
as a UMD module from unpkg or jsDelivr. The UMD builds have been compiled to ES5, so they should work in any modern browser. UMD builds are useful if you aren't using a package manager or module bundler. For example, you can use them directly in the <script>
tag of an HTML page.
The following builds are available:
NOTE: if you are using the CDN scripts in production, you should include a specific version number in the URL, for example:
When using a UMD build, you will have access to a global window.PDFLib
variable. This variable contains all of the classes and functions exported by pdf-lib
. For example:
import { PDFDocument, rgb } from 'pdf-lib';
var PDFDocument = PDFLib.PDFDocument;
var rgb = PDFLib.rgb;
Fontkit Installation
pdf-lib
relies upon a sister module to support embedding custom fonts: @pdf-lib/fontkit
. You must add the @pdf-lib/fontkit
module to your project and register it using pdfDoc.registerFontkit(...)
before embedding custom fonts (see the font embedding example). This module is not included by default because not all users need it, and it increases bundle size.
Installing this module is easy. Just like pdf-lib
itself, @pdf-lib/fontkit
can be installed with npm
/yarn
or as a UMD module.
Fontkit NPM Module
npm install --save @pdf-lib/fontkit
yarn add @pdf-lib/fontkit
To register the fontkit
instance:
import { PDFDocument } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
Fontkit UMD Module
The following builds are available:
NOTE: if you are using the CDN scripts in production, you should include a specific version number in the URL, for example:
When using a UMD build, you will have access to a global window.fontkit
variable. To register the fontkit
instance:
var pdfDoc = await PDFLib.PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
Documentation
API documentation is available on the project site at https://pdf-lib.js.org/docs/api/.
The repo for the project site (and generated documentation files) is
located here: https://github.com/Hopding/pdf-lib-docs.
Fonts and Unicode
When working with PDFs, you will frequently come across the terms "character encoding" and "font". If you have experience in web development, you may wonder why these are so prevalent. Aren't they just annoying details that you shouldn't need to worry about? Shouldn't PDF libraries and readers be able to handle all of this for you like web browsers can? Unfortunately, this is not the case. The nature of the PDF file format makes it very difficult to avoid thinking about character encodings and fonts when working with PDFs.
pdf-lib
does its best to simplify things for you. But it can't perform magic. This means you should be aware of the following:
- There are 14 standard fonts defined in the PDF specification. They are as follows: Times Roman (normal, bold, and italic), Helvetica (normal, bold, and italic), Courier (normal, bold, and italic), ZapfDingbats (normal), and Symbol (normal). These 14 fonts are guaranteed to be available in PDF readers. As such, you do not need to embed any font data if you wish to use one of these fonts. You can use a standard font like so:
import { PDFDocument, StandardFonts } from 'pdf-lib'
const pdfDoc = await PDFDocument.create()
const courierFont = await pdfDoc.embedFont(StandardFonts.Courier)
const page = pdfDoc.addPage()
page.drawText('Some boring latin text in the Courier font', {
font: courierFont,
})
- The standard fonts do not support all characters available in Unicode. The Times Roman, Helvetica, and Courier fonts use WinAnsi encoding (aka Windows-1252). The WinAnsi character set only supports 218 characters in the Latin alphabet. For this reason, many users will find the standard fonts insufficient for their use case. This is unfortunate, but there's nothing that PDF libraries can do to change this. This is a result of the PDF specification and its age. Note that the ZapfDingbats and Symbol fonts use their own specialized encodings that support 203 and 194 characters, respectively. However, the characters they support are not useful for most use cases. See here for an example of all 14 standard fonts.
- You can use characters outside the Latin alphabet by embedding your own fonts. Embedding your own font requires to you load the font data (from a file or via a network request, for example) and pass it to the
embedFont
method. When you embed your own font, you can use any Unicode characters that it supports. This capability frees you from the limitations imposed by the standard fonts. Most PDF files use embedded fonts. You can embed and use a custom font like so (see also):
import { PDFDocument } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
const url = 'https://pdf-lib.js.org/assets/ubuntu/Ubuntu-R.ttf'
const fontBytes = await fetch(url).then((res) => res.arrayBuffer())
const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
const ubuntuFont = await pdfDoc.embedFont(fontBytes)
const page = pdfDoc.addPage()
page.drawText('Some fancy Unicode text in the ŪЬȕǹƚü font', {
font: ubuntuFont,
})
Note that encoding errors will be thrown if you try to use a character with a font that does not support it. For example, Ω
is not in the WinAnsi character set. So trying to draw it on a page with the standard Helvetica font will throw the following error:
Error: WinAnsi cannot encode "Ω" (0x03a9)
at Encoding.encodeUnicodeCodePoint
Font Subsetting
Embedding a font in a PDF document will typically increase the file's size. You can reduce the amount a file's size is increased by subsetting the font so that only the necessary characters are embedded. You can subset a font by setting the subset
option to true
. For example:
const font = await pdfDoc.embedFont(fontBytes, { subset: true });
Note that subsetting does not work for all fonts. See https://github.com/Hopding/pdf-lib/issues/207#issuecomment-537210471 for additional details.
Creating and Filling Forms
pdf-lib
can create, fill, and read PDF form fields. The following field types are supported:
See the form creation and form filling usage examples for code samples. Tests 1, 14, 15, 16, and 17 in the complete examples contain working example code for form creation and filling in a variety of different JS environments.
IMPORTANT: The default font used to display text in buttons, dropdowns, option lists, and text fields is the standard Helvetica font. This font only supports characters in the latin alphabet (see Fonts and Unicode for details). This means that if any of these field types are created or modified to contain text outside the latin alphabet (as is often the case), you will need to embed and use a custom font to update the field appearances. Otherwise an error will be thrown (likely when you save the PDFDocument
).
You can use an embedded font when filling form fields as follows:
import { PDFDocument } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
const formUrl = 'https://pdf-lib.js.org/assets/dod_character.pdf';
const formBytes = await fetch(formUrl).then((res) => res.arrayBuffer());
const fontUrl = 'https://pdf-lib.js.org/assets/ubuntu/Ubuntu-R.ttf';
const fontBytes = await fetch(fontUrl).then((res) => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formBytes);
pdfDoc.registerFontkit(fontkit);
const ubuntuFont = await pdfDoc.embedFont(fontBytes);
const form = pdfDoc.getForm();
const nameField = form.getTextField('CharacterName 2');
const ageField = form.getTextField('Age');
nameField.setText('Ӎӑȑїõ');
ageField.setText('24 ŷȇȁŗš');
form.updateFieldAppearances(ubuntuFont);
const pdfBytes = await pdfDoc.save();
Handy Methods for Filling, Creating, and Reading Form Fields
Existing form fields can be accessed with the following methods of PDFForm
:
New form fields can be created with the following methods of PDFForm
:
Below are some of the most commonly used methods for reading and filling the aforementioned subclasses of PDFField
:
Limitations
pdf-lib
can extract the content of text fields (see PDFTextField.getText
), but it cannot extract plain text on a page outside of a form field. This is a difficult feature to implement, but it is within the scope of this library and may be added to pdf-lib
in the future. See
#93,
#137,
#177,
#329, and
#380.pdf-lib
can remove and edit the content of text fields (see PDFTextField.setText
), but it does not provide APIs for removing or editing text on a page outside of a form field. This is also a difficult feature to implement, but is within the scope of pdf-lib
and may be added in the future. See
#93,
#137,
#177,
#329, and
#380.pdf-lib
does not support the use of HTML or CSS when adding content to a PDF. Similarly, pdf-lib
cannot embed HTML/CSS content into PDFs. As convenient as such a feature might be, it would be extremely difficult to implement and is far beyond the scope of this library. If this capability is something you need, consider using Puppeteer.
Help and Discussion
Discussions is the best place to chat with us, ask questions, and learn more about pdf-lib!
See also MAINTAINERSHIP.md#communication and MAINTAINERSHIP.md#discord.
Encryption Handling
pdf-lib
does not currently support encrypted documents. You should not use pdf-lib
with encrypted documents. However, this is a feature that could be added to pdf-lib
. Please create an issue if you would find this feature helpful!
When an encrypted document is passed to PDFDocument.load(...)
, an error will be thrown:
import { PDFDocument, EncryptedPDFError } from 'pdf-lib'
const encryptedPdfBytes = ...
const pdfDoc = PDFDocument.load(encryptedPdfBytes)
This default behavior is usually what you want. It allows you to easily detect if a given document is encrypted, and it prevents you from trying to modify it. However, if you really want to load the document, you can use the { ignoreEncryption: true }
option:
import { PDFDocument } from 'pdf-lib'
const encryptedPdfBytes = ...
const pdfDoc = PDFDocument.load(encryptedPdfBytes, { ignoreEncryption: true })
Note that using this option does not decrypt the document. This means that any modifications you attempt to make on the returned PDFDocument
may fail, or have unexpected results.
You should not use this option. It only exists for backwards compatibility reasons.
Contributing
We welcome contributions from the open source community! If you are interested in contributing to pdf-lib
, please take a look at the CONTRIBUTING.md file. It contains information to help you get pdf-lib
setup and running on your machine. (We try to make this as simple and fast as possible! :rocket:)
Maintainership
Check out MAINTAINERSHIP.md for details on how this repo is maintained and how we use issues, PRs, and discussions.
Tutorials and Cool Stuff
Prior Art
pdfkit
is a PDF generation library for Node and the Browser. This library was immensely helpful as a reference and existence proof when creating pdf-lib
. pdfkit
's code for font embedding, PNG embedding, and JPG embedding was especially useful.pdf.js
is a PDF rendering library for the Browser. This library was helpful as a reference when writing pdf-lib
's parser. Some of the code for stream decoding was ported directly to TypeScript for use in pdf-lib
.pdfbox
is a PDF generation and modification library written in Java. This library was an invaluable reference when implementing form creation and filling APIs for pdf-lib
.jspdf
is a PDF generation library for the browser.pdfmake
is a PDF generation library for the browser.hummus
is a PDF generation and modification library for Node environments. hummus
is a Node wrapper around a C++ library, so it doesn't work in many JavaScript environments - like the Browser or React Native.react-native-pdf-lib
is a PDF generation and modification library for React Native environments. react-native-pdf-lib
is a wrapper around C++ and Java libraries.pdfassembler
is a PDF generation and modification library for Node and the browser. It requires some knowledge about the logical structure of PDF documents to use.
Git History Rewrite
This repo used to contain a file called pdf_specification.pdf
in the root directory. This was a copy of the PDF 1.7 specification, which is made freely available by Adobe. On 8/30/2021, we received a DMCA complaint requiring us to remove the file from this repo. Simply removing the file via a new commit to master
was insufficient to satisfy the complaint. The file needed to be completely removed from the repo's git history. Unfortunately, the file was added over two years ago, this meant we had to rewrite the repo's git history and force push to master
😔.
Steps We Took
We removed the file and rewrote the repo's history using BFG Repo-Cleaner as outlined here. For full transparency, here are the exact commands we ran:
$ git clone git@github.com:Hopding/pdf-lib.git
$ cd pdf-lib
$ rm pdf_specification.pdf
$ git commit -am 'Remove pdf_specification.pdf'
$ bfg --delete-files pdf_specification.pdf
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
$ git push --force
Why Should I Care?
If you're a user of pdf-lib
, you shouldn't care! Just keep on using pdf-lib
like normal 😃 ✨!
If you are a pdf-lib
developer (meaning you've forked pdf-lib
and/or have an open PR) then this does impact you. If you forked or cloned the repo prior to 8/30/2021 then your fork's git history is out of sync with this repo's master
branch. Unfortunately, this will likely be a headache for you to deal with. Sorry! We didn't want to rewrite the history, but there really was no alternative.
It's important to note that pdf-lib's source code has not changed at all. It's exactly the same as it was before the git history rewrite. The repo still has the exact same number of commits (and even the same commit contents, except for the commit that added pdf_specification.pdf
). What has changed are the SHAs of those commits.
The simplest way to deal with this fact is to:
- Reclone pdf-lib
- Manually copy any changes you've made from your old clone to the new one
- Use your new clone going forward
- Reopen your unmerged PRs using your new clone
See this StackOverflow answer for a great, in depth explanation of what a git history rewrite entails.
License
MIT